/*
 * Copyright 2025 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package androidx.core.telecom.reference

import android.app.Activity
import android.app.Notification
import android.app.NotificationChannel
import android.app.NotificationManager
import android.app.PendingIntent
import android.content.Context
import android.content.Intent
import android.net.Uri
import android.util.Log
import androidx.core.app.NotificationCompat
import androidx.core.app.Person
import androidx.core.graphics.drawable.IconCompat
import androidx.core.telecom.CallAttributesCompat
import androidx.core.telecom.reference.Constants.ACTION_ANSWER_AND_SHOW_UI
import androidx.core.telecom.reference.Constants.ACTION_DECLINE_CALL
import androidx.core.telecom.reference.Constants.ACTION_HANGUP_CALL
import androidx.core.telecom.reference.Constants.DEEP_LINK_BASE_URI
import androidx.core.telecom.reference.Constants.EXTRA_CALL_ID
import androidx.core.telecom.reference.Constants.EXTRA_REMOTE_USER_NAME
import androidx.core.telecom.reference.Constants.EXTRA_SIMULATED_NUMBER
import androidx.core.telecom.reference.model.CallData
import androidx.core.telecom.reference.model.CallState
import androidx.core.telecom.reference.service.TelecomVoipService
import androidx.core.telecom.reference.view.DialerActivity
import androidx.core.telecom.reference.view.NavRoutes

/**
 * Manages the creation, display, update, and cancellation of call-related notifications using
 * [NotificationCompat.CallStyle].
 *
 * This class handles the lifecycle of notifications associated with VoIP calls managed by
 * [androidx.core.telecom]. It ensures the correct notification style and actions (Answer, Decline,
 * Hangup) are presented based on the call's state ([CallState]).
 *
 * Key responsibilities include:
 * - Creating a dedicated notification channel for call notifications.
 * - Building `NotificationCompat.Builder` instances configured with [NotificationCompat.CallStyle]
 *   for various call states (incoming, outgoing, active, held, etc.).
 * - Generating appropriate [PendingIntent]s for user actions (answering, declining, hanging up,
 *   opening the in-call UI).
 * - Posting notifications to the [NotificationManager] based on [CallData] updates.
 * - Cancelling notifications when a call is disconnected or should no longer be displayed.
 * - Handling deep links to navigate the user to the correct in-call screen within the host
 *   Activity.
 *
 * It interacts primarily with [CallData] to get call attributes and state, and uses [Context] to
 * access system services like [NotificationManager] and resources. Actions like declining or
 * hanging up typically trigger intents targeting a background service (e.g., [TelecomVoipService]),
 * while answering or tapping the notification body targets the main application UI (specified by
 * [hostActivityClass]).
 *
 * @param context The application context, used for accessing system services and resources.
 * @param hostActivityClass The class of the Activity that hosts the main call UI. This Activity
 *   will be launched via PendingIntents when the user interacts with the notification (e.g., taps
 *   the body or the 'Answer' action). It should be configured to handle the deep links and actions
 *   generated by this manager. Defaults to [DialerActivity].
 * @see NotificationCompat.CallStyle
 * @see NotificationManager
 * @see CallData
 * @see CallState
 * @see TelecomVoipService
 */
class CallNotificationManager(
    private val context: Context,
    private val hostActivityClass: Class<out Activity> = DialerActivity::class.java,
) {

    private val notificationManager =
        context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager

    companion object {
        private const val TAG = "CallNotificationMgr"
        // Notification Constants
        private const val CALL_NOTIFICATION_CHANNEL_ID = "VOIP_CALL_CHANNEL"
        private const val CALL_NOTIFICATION_CHANNEL_NAME = "VoIP Calls"
        // Icons
        private val DEFAULT_PERSON_ICON = R.drawable.ic_call_white_24dp
        private val DEFAULT_SMALL_ICON = R.drawable.ic_notification_default_white_24d
    }

    init {
        createNotificationChannel()
    }

    // --- Public Methods ---
    fun buildIncomingCallNotification(id: Int, attributes: CallAttributesCompat): Notification? {
        return buildNotificationInternal(id, attributes, CallState.RINGING)?.build()
    }

    fun buildOutgoingCallNotification(id: Int, attributes: CallAttributesCompat): Notification? {
        return buildNotificationInternal(id, attributes, CallState.DIALING)?.build()
    }

    /**
     * Displays or updates the call-style notification for the given CallData state. If the state
     * results in a valid notification, it's posted via NotificationManager. If the state is invalid
     * (e.g., invalid CallAttributes), any existing notification is cancelled.
     */
    fun showOrUpdateCallNotification(callData: CallData) {
        val notificationId = callIdToNotificationId(callData.callId)
        Log.d(
            TAG,
            "[${callData.callId}] Requesting show/update notification (State: ${callData.callState})",
        )

        // Delegate to the internal builder logic
        val builder =
            buildNotificationInternal(
                callData.callId.toInt(),
                callData.attributes,
                callData.callState,
            )

        if (builder != null) {
            // If builder is valid, build and notify
            Log.i(
                TAG,
                "[${callData.callId}] Posting notification (ID:[$notificationId], State: ${callData.callState})",
            )
            notificationManager.notify(notificationId, builder.build())
        } else {
            // If builder is null (e.g., invalid CallAttributes), ensure cancellation happens.
            // The internal builder might have already logged, but we ensure cancel here.
            Log.i(
                TAG,
                "[${callData.callId}] No valid notification to show for state ${callData.callState}, ensuring cancellation.",
            )
            cancelCallNotification(callData.callId) // Explicit cancel for invalid states
        }
    }

    /** Immediately posts the Call-Style notification to the status bar* */
    fun immediatelyPostNotification(id: Int, notification: Notification) {
        Log.i(TAG, "[immediatelyPostNotification: Posting notification (ID: [$id]})")
        notificationManager.notify(id, notification)
    }

    /** Cancels the notification associated with the given call ID. */
    fun cancelCallNotification(callId: String) {
        val notificationId = callIdToNotificationId(callId)
        Log.d(TAG, "[$callId] Cancelling notification (ID: $notificationId).")
        notificationManager.cancel(notificationId)
    }

    // --- Private Helper Methods ---

    /**
     * Creates the notification channel required for call notifications on Android O+. Should be
     * called once, e.g., when the service or application starts.
     */
    private fun createNotificationChannel() {
        val channel =
            NotificationChannel(
                    CALL_NOTIFICATION_CHANNEL_ID,
                    CALL_NOTIFICATION_CHANNEL_NAME,
                    NotificationManager.IMPORTANCE_HIGH, // Use HIGH for incoming calls/heads-up
                )
                .apply {
                    description = "Notifications for incoming and ongoing VoIP calls"
                    lockscreenVisibility = Notification.VISIBILITY_PUBLIC
                    // Sound and vibration should be handled by the app/Telecom, not the channel
                    // itself
                    setSound(null, null)
                    enableVibration(false)
                }
        notificationManager.createNotificationChannel(channel)
        Log.d(TAG, "Notification channel created: $CALL_NOTIFICATION_CHANNEL_ID")
    }

    /**
     * Internal helper to configure a NotificationCompat.Builder based on CallData. Returns the
     * configured builder if a notification should be shown for the state, otherwise returns null.
     */
    private fun buildNotificationInternal(
        notificationId: Int,
        attributes: CallAttributesCompat,
        callState: CallState = CallState.UNKNOWN,
    ): NotificationCompat.Builder? {
        val callId = notificationId.toString()
        // Ensure we have valid data to proceed
        if (attributes.displayName.isBlank()) {
            Log.e(TAG, "Cannot build notification with invalid attributes: $attributes")
            return null
        }

        val contentPendingIntent = createContentPendingIntent(callId, notificationId)
        val callerPerson = createPerson(attributes)

        val builder =
            NotificationCompat.Builder(context, CALL_NOTIFICATION_CHANNEL_ID)
                .setSmallIcon(DEFAULT_SMALL_ICON)
                .setContentTitle(attributes.displayName)
                .setContentText(getNotificationContentText(callState))
                .setPriority(NotificationCompat.PRIORITY_MAX) // Max priority for calls
                .setCategory(NotificationCompat.CATEGORY_CALL) // Essential for call notifications
                .setContentIntent(contentPendingIntent) // Intent when notification body is clicked
                .setOngoing( // Notification cannot be dismissed by user if call is
                    // active/ringing/dialing
                    callState != CallState.DISCONNECTED && callState != CallState.UNKNOWN
                )
                .setVisibility(NotificationCompat.VISIBILITY_PUBLIC) // Show on lock screen
                .setSilent(true) // We handle sound/vibration elsewhere (Telecom or app logic)
                .setFullScreenIntent(contentPendingIntent, true)

        val callStyle: NotificationCompat.CallStyle? =
            when (callState) {
                CallState.RINGING -> {
                    builder.setUsesChronometer(false) // No timer when ringing
                    NotificationCompat.CallStyle.forIncomingCall(
                        callerPerson,
                        createDeclinePendingIntent(callId, notificationId),
                        createAnswerPendingIntent(callId, notificationId, attributes),
                    )
                }
                CallState.DIALING -> {
                    builder.setUsesChronometer(false) // No timer when dialing
                    // Use forOutgoingCall style for dialing state
                    NotificationCompat.CallStyle.forOngoingCall(
                        callerPerson,
                        createHangupPendingIntent(
                            callId,
                            notificationId,
                        ), // Hangup action for outgoing
                    )
                }
                CallState.ACTIVE -> {
                    builder
                        .setUsesChronometer(true) // Show timer when active
                        .setWhen(
                            System.currentTimeMillis()
                        ) // Set timer base to now (or actual call start time if available)
                        .setChronometerCountDown(false)
                    NotificationCompat.CallStyle.forOngoingCall(
                        callerPerson,
                        createHangupPendingIntent(callId, notificationId),
                    )
                }
                CallState.INACTIVE -> { // e.g. On Hold
                    builder.setUsesChronometer(false) // No timer when on hold
                    // You might want different actions for hold state? For now, just Hangup.
                    NotificationCompat.CallStyle.forOngoingCall(
                        callerPerson,
                        createHangupPendingIntent(callId, notificationId),
                    )
                }
                // DISCONNECTED and UNKNOWN states should not show a notification
                CallState.DISCONNECTED,
                CallState.UNKNOWN -> {
                    null
                }
            }
        return if (callStyle != null) {
            builder.setStyle(callStyle)
            builder // Return the configured builder
        } else {
            builder
        }
    }

    // Creates the main PendingIntent for clicking the notification body
    private fun createContentPendingIntent(callId: String, notificationId: Int): PendingIntent {
        // Target the In-Call screen via deep link or direct Activity intent
        val deepLinkUri =
            Uri.parse(
                "$DEEP_LINK_BASE_URI/${NavRoutes.IN_CALL}/$callId"
            ) // Include callId in URI path

        // Intent to launch the host Activity which handles the deep link
        val intent =
            Intent(Intent.ACTION_VIEW, deepLinkUri, context, hostActivityClass).apply {
                putExtra(EXTRA_CALL_ID, callId)
                flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
            }
        val requestCode = notificationId + 4

        return PendingIntent.getActivity(
            context,
            requestCode,
            intent,
            PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE,
        )
    }

    // Creates the Person object representing the caller/callee
    private fun createPerson(attributes: CallAttributesCompat): Person {
        val iconCompat = IconCompat.createWithResource(context, DEFAULT_PERSON_ICON)
        return Person.Builder()
            .setName(attributes.displayName)
            .setUri(attributes.address.toString())
            .setIcon(iconCompat)
            .setImportant(true)
            .build()
    }

    // Consistently generates a notification ID from a call ID string
    private fun callIdToNotificationId(callId: String): Int {
        return callId.toInt()
    }

    // Provides human-readable text for the notification content based on state
    private fun getNotificationContentText(state: CallState): String {
        return when (state) {
            CallState.RINGING -> "Incoming Call"
            CallState.DIALING -> "Dialing..."
            CallState.ACTIVE -> "Active Call"
            CallState.INACTIVE -> "Call on Hold"
            CallState.DISCONNECTED -> "Call Ended"
            CallState.UNKNOWN -> "Call Error"
        }
    }

    // --- PendingIntents for Notification Actions ---

    private fun createServiceActionIntent(callId: String, action: String): Intent {
        return Intent(context, TelecomVoipService::class.java).apply {
            this.action = action
            putExtra(EXTRA_CALL_ID, callId) // Pass call ID to the service
        }
    }

    private fun createAnswerPendingIntent(
        callId: String,
        notificationId: Int,
        attributes: CallAttributesCompat,
    ): PendingIntent {
        // Use the same deep link logic as createContentPendingIntent
        val deepLinkUri = Uri.parse("$DEEP_LINK_BASE_URI/${NavRoutes.IN_CALL}/$callId")

        // Create an Intent targeting the Activity
        val intent =
            Intent(Intent.ACTION_VIEW, deepLinkUri, context, hostActivityClass).apply {
                // Set the custom action to distinguish this from a normal notification tap
                action = ACTION_ANSWER_AND_SHOW_UI
                // Add flags: NEW_TASK is needed if the activity isn't running,
                flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
                // Ensure callId is available, either via deep link parsing or as an extra
                putExtra(EXTRA_CALL_ID, callId) // Explicitly add callId as extra for easy access
                // Add any other extras the Activity might need immediately from attributes
                putExtra(EXTRA_SIMULATED_NUMBER, attributes.address.schemeSpecificPart)
                putExtra(EXTRA_REMOTE_USER_NAME, attributes.displayName)
            }

        val requestCode = notificationId + 1 // Keep request code unique
        return PendingIntent.getActivity(
            context,
            requestCode,
            intent,
            PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE,
        )
    }

    private fun createDeclinePendingIntent(callId: String, notificationId: Int): PendingIntent {
        val intent = createServiceActionIntent(callId, ACTION_DECLINE_CALL)
        val requestCode = notificationId + 2
        return PendingIntent.getService(
            context,
            requestCode,
            intent,
            PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE,
        )
    }

    private fun createHangupPendingIntent(callId: String, notificationId: Int): PendingIntent {
        val intent = createServiceActionIntent(callId, ACTION_HANGUP_CALL)
        val requestCode = notificationId + 3
        return PendingIntent.getService(
            context,
            requestCode,
            intent,
            PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE,
        )
    }
}
